/*
 * Copyright (c) 2020 Fraunhofer FOKUS and others. All rights reserved.
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contact: mosaic@fokus.fraunhofer.de
 */
package org.eclipse.mosaic.lib.geo;

import org.eclipse.mosaic.lib.math.Vector3d;
import org.eclipse.mosaic.lib.transform.GeoProjection;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class GeoSector implements Sector<GeoPoint>, GeoArea {

    private static final long serialVersionUID = 1L;

    private final GeoPoint center;
    private final double radius;

    /**
     * north = 0 south = 180
     */
    private final double begin;
    private final double end;

    public GeoSector(GeoPoint center, double radius, double begin, double end) {
        this.center = center;
        this.radius = radius;
        this.begin = begin;
        this.end = end;
    }

    @Override
    public Bounds<GeoPoint> getBounds() {
        double angleDeg = end - begin;
        Vector3d direction1;
        Vector3d direction2;

        if (angleDeg > 0 && angleDeg <= 90) {
            double delta = Math.toDegrees(Math.atan(Math.sin(Math.toRadians(angleDeg))));
            direction1 = new Vector3d(0, 0, 0);
            direction2 = new Vector3d(radius / Math.cos(Math.toRadians(delta)) * Math.sin(Math.toRadians(begin + delta)), 0,
                    - radius / Math.cos(Math.toRadians(delta)) * Math.cos(Math.toRadians(begin + delta)));
        } else if (angleDeg > 90 && angleDeg <= 180) {
            double delta = (180 - angleDeg) / 2;
            double gama = Math.toDegrees(Math.atan(Math.cos(Math.toRadians(delta))));
            direction1 = new Vector3d(radius * Math.cos(Math.toRadians(delta)) * Math.sin(Math.toRadians(begin - delta)), 0,
                    - radius * Math.cos(Math.toRadians(delta)) * Math.cos(Math.toRadians(begin - delta)));
            double radians = Math.toRadians(begin - delta + 90 + gama);
            direction2 = new Vector3d(radius / Math.cos(Math.toRadians(gama)) * Math.sin(radians), 0,
                    - radius / Math.cos(Math.toRadians(gama)) * Math.cos(radians));
        } else if (angleDeg > 180 && angleDeg < 360) {
            double delta = (360 - angleDeg) / 2;
            double gama = Math.toDegrees(Math.atan(Math.cos(Math.toRadians(delta))));

            double r = radius / Math.cos(Math.toRadians(gama));
            double radians = Math.toRadians(begin + 90 - gama - delta);
            direction1 = new Vector3d(r * Math.sin(radians), 0, - r * Math.cos(radians));

            r = Math.sqrt(2) * radius;
            radians = Math.toRadians(begin + 90 - delta + 135);
            direction2 = new Vector3d(r * Math.sin(radians), 0, - r * Math.cos(radians));
        } else {
            throw new IllegalArgumentException(String.format("begin: %.5f, end: %.5f, cannot find Bounds", begin, end));
        }

        GeoPoint point1 = GeoProjection.getInstance().getGeoCalculator().pointFromDirection(
                getCenter(), direction1, new MutableGeoPoint()
        );
        GeoPoint point2 = GeoProjection.getInstance().getGeoCalculator().pointFromDirection(
                getCenter(), direction2, new MutableGeoPoint()
        );

        return new GeoRectangle(point1, point2);
    }

    @Override
    public CartesianArea toCartesian() {
        return new CartesianSector(center.toCartesian(), radius, begin, end);
    }

    @Override
    public GeoPoint getCenter() {
        return center;
    }

    @Override
    public double getRadius() {
        return radius;
    }

    @Override
    public double getBegin() {
        return begin;
    }

    @Override
    public double getEnd() {
        return end;
    }

    @Override
    public double getRadians() {
        return Math.toRadians(end - begin);
    }

    @Override
    public Polygon<GeoPoint> toPolygon() {
        return toPolygon(1);
    }

    public GeoPolygon toPolygon(double degreeResolution) {
        final List<GeoPoint> vertices = new ArrayList<>();
        vertices.add(center);

        final Vector3d direction = new Vector3d();
        int steps = (int) ((end - begin) / degreeResolution);
        for (int i = 0; i <= steps; i++) {
            double radians = Math.toRadians(begin + i * degreeResolution);
            direction.set(radius * Math.sin(radians), 0, - getRadius() * Math.cos(radians));
            vertices.add(GeoProjection.getInstance().getGeoCalculator().pointFromDirection(
                    getCenter(), direction, new MutableGeoPoint()
            ));
        }
        return new GeoPolygon(vertices);
    }

    @Override
    public boolean contains(GeoPoint point) {
        double degree = GeoUtils.azimuth(center, point);

        return getCenter().distanceTo(point) <= getRadius() && degree >= begin && degree <= end;
    }

    @Override
    public String toString() {
        return String.format(Locale.ENGLISH, "GeoSector{center=%s,radius=%.2f,begin=%.2f,end=%.2f}", this.center,
                this.radius, this.begin, this.end);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        GeoSector other = (GeoSector) o;
        return Double.compare(radius, other.getRadius()) == 0
                && this.getCenter().equals(other.getCenter())
                && this.getBegin() == other.getBegin()
                && this.getEnd() == other.getEnd();
    }

    @Override
    public int hashCode() {
        long longHash = 20816225L;
        longHash = 31 * longHash + Double.doubleToLongBits(radius);
        longHash = 31 * longHash + getCenter().hashCode();
        longHash = 31 * longHash + Double.doubleToLongBits(begin);
        longHash = 31 * longHash + Double.doubleToLongBits(end);
        return (int) (longHash ^ (longHash >>> 32));
    }
}
